from pyprotobuf.commentresolver import CommentResolver
from pyprotobuf.dependencysorter import DependencySorter
from pyprotobuf.extensionsresolver import ExtensionsResolver
from pyprotobuf.nameresolver import NameResolver
from pyprotobuf.optionresolver import OptionResolver
from pyprotobuf.parser import ProtoParser
import copy
import logging
import os
import pyprotobuf.nodes as nodes


logger = logging.getLogger('pyprotobuf.Compiler')


class CompilerInput(object):
    filename = None
    ast = None

    def __init__(self, filename):
        self.filename = filename

    def get_source(self):
        return open(self.filename, 'r').read()


class StringCompilerInput(CompilerInput):
    def __init__(self, filename, data):
        super(StringCompilerInput, self).__init__(filename)
        self.data = data

    def get_source(self):
        return self.data


class Compiler(object):
    logger = logging.getLogger('pyprotobuf.Compiler')

    passes = [
        OptionResolver,
        NameResolver,
        ExtensionsResolver,
        DependencySorter,
        CommentResolver
    ]

    def __init__(self):
        self.parser = ProtoParser()
        self.import_paths = [os.getcwd(), os.path.join(os.path.dirname(os.path.abspath(__file__)), 'proto')]
        self.imports = {}
        self.log_level = logging.INFO
        self._inputs = []

    def set_inputs(self, inputs):
        """
            :type inputs: list(CompilerInput)
        """
        self._inputs = inputs

    def compile(self):
        """ Return a RootNode containing all the PackageNode->FileNode
            :rtype: pyprotobuf.nodes.RootNode
        """
        #
        self.root = nodes.RootNode()
        self.traversal_path = []

        for input in self._inputs:
            input.ast = file_node = self._precompile_file(input.get_source(), input.filename)
            self._add_file_to_package(file_node)

        for compiler_pass_class in self.passes:
            compiler_pass = compiler_pass_class(self)
            compiler_pass.set_log_level(self.log_level)
            compiler_pass.process(self.root)

        return self.root

    def _add_file_to_package(self, filenode):
        # resolve an add other packages before adding ours
        self._resolve_imports(filenode)

        package_name = filenode.package_name

        package = self.root.get_package(package_name)

        if not package:
            package = nodes.Package()
            package.name = package_name
            self.root.add_child(package)

        package.add_child(filenode)

    def _precompile_file(self, string, filename):
        file_node = self.parser.parse_string(string)
        # so the generators know the path
        file_node.filename = filename
        return file_node

    def _resolve_import_path(self, path, file_node=None):
        """
            :param path: Path to resolve
            :param file_node: File node to resolve relative paths from.
        """
        if path.startswith('/'):
            return path

        # search through each of the paths
        # if specified, starting relative to the directory of the package
        import_paths = copy.copy(self.import_paths)
        if file_node is not None:
            assert isinstance(file_node, nodes.FileNode)
            import_paths.insert(0, os.path.dirname(file_node.filename))

        split_path = path.split(os.pathsep)
        for import_path in import_paths:
            test_path = os.path.join(import_path, *split_path)
            if os.path.exists(test_path):
                return test_path

        raise Exception("Could not find import %s in paths %s" % (path, import_paths))

    def _resolve_imports(self, file_node):
        """
            :type file_node: pyprotobuf.nodes.FileNode
        """
        assert isinstance(file_node, nodes.FileNode)


        # find any import nodes
        for child in file_node.get_children_of_type(nodes.ImportNode):
            path = self._resolve_import_path(child.value, file_node)

            if path in self.imports:
                file_node = self.imports[path]
            else:
                file_node = self._import_file(path)
                self.logger.info('Importing %s from %s: %s %s', path, file_node.filename, file_node, file_node.children)
                self.imports[path] = file_node

            # attach the imported FileNode to the ImportNode
            # XXX: unused
            child.file_node = file_node

            if path in self.traversal_path:
                self.traversal_path.append(path)
                raise Exception("Circular import %s" % self.traversal_path)

            # visit other imports before we add ours
            self.traversal_path.append(path)
            self._resolve_imports(file_node)
            self.traversal_path.pop()
            self._add_file_to_package(file_node)

    def _import_file(self, filename):
        """ Return the FileNode node at path.
            :rtype: pyprotobuf.nodes.FileNode
        """
        if filename in self.traversal_path:
            raise Exception("Circular import %s from %s" % (filename, self.traversal_path))

        self.traversal_path.append(filename)

        with open(filename, 'r') as f:
            string = f.read()

        filenode = self.parser.parse_string(string)
        filenode.filename = filename

        self._resolve_imports(filenode)

        self.traversal_path.pop()

        return filenode
